import os, bs4, tiktoken
from dotenv import load_dotenv

from langchain_community.chat_models import ChatZhipuAI

from langchain_community.document_loaders import WebBaseLoader

from langchain_text_splitters import RecursiveCharacterTextSplitter

from zhipuai import ZhipuAI

from langchain_chroma import Chroma

from langchain import hub

from langchain_core.output_parsers import StrOutputParser

from langchain_core.runnables import RunnablePassthrough

from langchain.prompts import PromptTemplate

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import SystemMessage, HumanMessage

from langchain.chains.history_aware_retriever import create_history_aware_retriever

from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains import create_retrieval_chain
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables import RunnableWithMessageHistory

from sqlalchemy import Column, Integer, String, Text, ForeignKey, create_engine
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import relationship, sessionmaker
import atexit

from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, ForeignKey, create_engine, DateTime

import hashlib

load_dotenv()

# 明确设置环境变量以避免 LangSmith 警告
os.environ["LANGCHAIN_TRACING_V2"] = "false"
os.environ["USER_AGENT"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 RAG Application"


class DocumentProcessor:
    def __init__(self, page_url, class_tuple):
        self.page_url = page_url
        self.class_tuple = class_tuple
    def web_page_load(self):
        header_template = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
        }
        loader = WebBaseLoader(
            web_paths=[self.page_url],
            header_template = header_template,
            bs_kwargs={
                "parse_only": bs4.SoupStrainer(class_=self.class_tuple),
            },

            # 去除文本两端的空白字符：当设置strip=True时，BeautifulSoup在提取文本内容后会自动去除文本开头和结尾的空白字符，包括空格、制表符、换行符等。
            # 清理文本内容：这有助于清理从网页中提取的文本，去除不必要的空白字符，使文本更加整洁。
            bs_get_text_kwargs={"strip": True}
        )

        docs = loader.load()

        # print(docs)

        # 检查是否有返回值
        if not docs:
            raise ValueError("无法从网页加载内容，请检查URL或CSS类名是否正确")
        
        # 检查返回的文档是否有实际内容
        valid_docs = [doc for doc in docs if doc.page_content and doc.page_content.strip()]
        if not valid_docs:
            raise ValueError("网页内容为空或无法解析，请检查网页结构")
            
        return docs

    def __token_length(self, text):
        """计算文本的token数量
            用来确保文本分割时按照token数量而不是字符数量来计算长度，这对于后续使用大型语言模型处理文本非常重要，因为LLM通常有token数量限制。
            通过这种方式，您可以确保分割的文本块不会超过模型的token限制，从而避免处理错误。
        """

        # 将文本编码为token序列，便于计算输入文本的token数量
        # 避免输入文本过长超出模型处理能力
        # 准确估计文本的token数量，以便更好地与语言模型交互
        encoding = tiktoken.get_encoding("cl100k_base")
        # print(f"计算文本长度: {text[:30]}...")  # 调试信息，显示前30个字符
        
        token_count = len(encoding.encode(text))
        # print(f"Token数量: {token_count}")
        return token_count


    def get_splits(self, docs):
        text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200, length_function=self.__token_length)
        # RecursiveCharacterTextSplitter.from_tiktoken_encoder(chunk_size=1000, chunk_overlap=200)

        splits_document = text_splitter.split_documents(docs)
        # 返回列表，列表中包含多个对象，返回结果类似：
        # [
        #   Document(page_content="文章的前1000个token...", metadata={...}),
        #   Document(page_content="从第800个token开始的文章内容...", metadata={...}),  # 因为有200个token的重叠
        #   Document(page_content="接下来的1000个token...", metadata={...}),
        #   ...
        # ]
        
        # 检查是否有返回值
        if not splits_document:
            raise ValueError("文档分割失败，没有生成任何文本块")
        
        # 检查分割后的文档是否有实际内容
        valid_splits = [doc for doc in splits_document if doc.page_content and doc.page_content.strip()]
        if not valid_splits:
            raise ValueError("分割后的文档内容为空")
        
        # 直接返回 Document 对象列表，而不是提取 page_content
        return splits_document   
    def process(self):
        """一站式处理：加载网页并分割文档"""
        docs = self.web_page_load()
        splits_list = self.get_splits(docs)
        return splits_list

class EmbeddingGenerator:
    """将拆分后的文档或者用户的问题转为向量数据"""
    def __init__(self, model_name):
        self.model_name = model_name  # 模型编码：embedding-2、embedding-3

        # import openai
        # self.client = OpenAI()
        self.client = ZhipuAI(api_key=os.getenv("ZhiPuAI_API_KEY"))
    
    def embed_documents(self, texts):
        """获取嵌入向量 - 分批处理避免请求过大"""
        # 如果传入的是 Document 对象列表，提取文本内容
        if hasattr(texts[0], 'page_content'): # 返回的布尔值，如果满足条件，则返回True，不满足返回False 
            texts = [doc.page_content for doc in texts]
        
        # 过滤空文本
        # 这是一个列表推导式，用于过滤掉空文本或只包含空白字符的文本：
        # text：遍历原始texts列表中的每个元素
        # if text and text.strip()：筛选条件
        # text：确保text不是None、空字符串等值
        # text.strip()：去除文本首尾空白字符后，确保还有实际内容
        # 这样可以去除空字符串、只包含空格或换行符的文本等无效内容。
        texts = [text for text in texts if text and text.strip()]
        
        if not texts:
            return [] # 如果没有有效文本，返回空列表，可以阻断后续代码执行
            
        response_vectors = []
        try:
            # 分批处理，避免单次请求过大
            batch_size = 10  # 每批处理10个文本，转化成向量数据
            for i in range(0, len(texts), batch_size): # range(0, len(texts), batch_size) 生成批次的起始索引(0, 10, 20, ...)
                batch_texts = texts[i:i + batch_size] #[0, 10),[10, 20),[20, 30)...最后一组数据如果切片超出了范围，但Python会自动处理
                
                if not batch_texts:
                    continue
                
                # 模型编码：embedding-2、embedding-3
                # 文本向量模型，将输入的文本信息进行向量化表示，以便于结合向量数据库为大模型提供外部知识库
                response = self.client.embeddings.create(
                    model=self.model_name,
                    input=batch_texts,
                ) # response返回值格式查看该网站：https://open.bigmodel.cn/dev/api/vector/embedding
                for response_data in response.data:
                    # print(f"获取embeding维度: {len(response_data.embedding)}")
                    response_vectors.append(response_data.embedding)
        except Exception as e:
            print(f"获取嵌入向量出错Error: {e}")
        else:     
            return response_vectors

    def embed_query(self, query_text):
        """获取用户的问题，转变为嵌入向量"""
        try:
            response = self.client.embeddings.create(
            model=self.model_name, # 填写需要调用的模型编码
            input=[query_text],
            )
            query_vectors = response.data[0].embedding
        except Exception as e:
            print(f"获取用户query嵌入向量出错Error: {e}")
        else:     
            # return query_embedding
            return query_vectors


class DocumentHashUtil:
    """文档哈希工具类，提供多种哈希算法""" 
    @classmethod
    def md5_hash(cls, content, length=16):
        """
        使用MD5算法计算文档哈希值
        
        Args:
            content (str): 文档内容
            length (int): 返回哈希值的长度，默认16位
            
        Returns:
            str: 文档的MD5哈希值
        """
        if not isinstance(content, str):
            raise TypeError("文档内容必须是字符串类型")
            
        content_bytes = content.encode("utf-8")
        full_hash = hashlib.md5(content_bytes).hexdigest()
        return full_hash[:length]
    
    @classmethod
    def sha256_hash(cls, content, length=16):
        """
        使用SHA256算法计算文档哈希值
        
        Args:
            content (str): 文档内容
            length (int): 返回哈希值的长度，默认16位
            
        Returns:
            str: 文档的SHA256哈希值
        """
        if not isinstance(content, str):
            raise TypeError("文档内容必须是字符串类型")
            
        content_bytes = content.encode("utf-8")
        full_hash = hashlib.sha256(content_bytes).hexdigest()
        return full_hash[:length]
    
    @classmethod
    def sha1_hash(cls, content, length=16):
        """
        使用SHA1算法计算文档哈希值
        
        Args:
            content (str): 文档内容
            length (int): 返回哈希值的长度，默认16位
            
        Returns:
            str: 文档的SHA1哈希值
        """
        if not isinstance(content, str):
            raise TypeError("文档内容必须是字符串类型")
            
        content_bytes = content.encode("utf-8")
        full_hash = hashlib.sha1(content_bytes).hexdigest()
        return full_hash[:length]

    @classmethod
    def get_hash_method(cls, method_name):
        """
        根据方法名获取对应的哈希方法
        
        Args:
            method_name (str): 哈希方法名
            
        Returns:
            callable: 对应的哈希方法
        """
        methods = {
            "md5": cls.md5_hash,
            "sha256": cls.sha256_hash,
            "sha1": cls.sha1_hash
        }
        
        if method_name not in methods:
            raise ValueError(f"不支持的哈希方法: {method_name}")
        
        return methods[method_name]

class ChromaStore:
    """
    创建向量数据库，将转化成向量数据的文档存储在向量数据库中，并返回向量检索器
    """
    @classmethod    
    def get_retriever(cls, model_name, splits_documents, persist_directory=None):
        embedding_generator = EmbeddingGenerator(model_name)
        
        # Chroma参数
        chroma_params = {
            "collection_name": "my_collection",
            "embedding_function": embedding_generator,
            "create_collection_if_not_exists": True
        }
        
        # 如果指定了持久化目录，则添加该参数
        if persist_directory:
            chroma_params["persist_directory"] = persist_directory
            
        chroma_store = Chroma(**chroma_params)

        # 检查并添加文档（使用哈希值去重）
        cls._add_documents_with_hash_deduplication(chroma_store, splits_documents)
        
        retriever = chroma_store.as_retriever(
            search_type="similarity",  # 相似性搜索
            search_kwargs={"k": 3}     # 返回最相似的3个文档
        )
        return retriever
    
    @classmethod
    def _add_documents_with_hash_deduplication(cls, chroma_store, splits_documents, hash_method="md5"):
        """
        使用哈希值去重的方式添加文档到向量数据库
        
        Args:
            chroma_store: Chroma数据库实例
            splits_documents: 分割后的文档列表

            hash_method (str): 哈希方法名，默认为'md5'
        """
        try:
            # 获取向量数据库中现有文档的数量
            existing_docs = chroma_store.get()
            # chroma_store.get() 方法,具体用法和返回结果参照chroma_methods.py

            # existing_docs.get("ids") = chroma_store.get().get("ids"),返回ids列表
            # 获取existing_docs字典中"ids"的键对应的值，因为用的是get()方法,所以获取不到值返回None
            existing_count = len(existing_docs["ids"]) if existing_docs.get("ids") else 0
            
            # 获取哈希方法,hash_func有两个参数,一个是文档内容,一个是返回的哈希值长度,默认16位
            hash_func = DocumentHashUtil.get_hash_method(hash_method)

            # 如果向量数据库为空，直接添加所有新文档
            if existing_count == 0:
                print(f"数据库为空，添加 {len(splits_documents)} 个新文档")

                # 为文档生成基于内容拼接的哈希的ID列表
                doc_ids = []
                for i, doc in enumerate(splits_documents):
                    # enumerate(splits_documents) 生成的序列：
                    # (0, Document(page_content="这是第一个文档内容...", metadata={}))
                    # (1, Document(page_content="这是第二个文档内容...", metadata={}))
                    # (2, Document(page_content="这是第三个文档内容...", metadata={}))
                    # ...
                    
                    content_hash = hash_func(doc.page_content)
                    # 前16位MD5 (64位)
                    # 理论冲突概率：约 1/2^64 ≈ 1/1.8×10^19
                    # 对于大多数应用仍然非常低，根据生日悖论，当文档数量达到 2^32 (约42亿) 时，冲突概率约为50%。

                    # 添加哈希值到元数据
                    if not doc.metadata:
                        doc.metadata = {}
                    doc.metadata["content_hash"] = content_hash

                    doc_id = f"doc_{content_hash}_{i}"
                    doc_ids.append(doc_id)
                
                # 将文档添加到向量数据库并转成向量
                chroma_store.add_documents(documents=splits_documents, ids=doc_ids)
                print(f"成功添加 {len(splits_documents)} 个文档")
                return
            
            # 如果数据库不为空，检查重复内容
            print(f"数据库中已存在 {existing_count} 个文档，检查重复内容...")
            
            # 获取向量数据库中已存在文档documents和元数据metadatas
            existing_docs_full = chroma_store.get(include=["metadatas", "documents"])
            # {'ids': ['ids1', 'ids2', ...], 'embeddings': None, 
            # 'documents': ['文档1', '文档2', ...], 
            # 'uris': None, 'included': ['metadatas', 'documents'], 
            # 'data': None, 
            # 'metadatas': [{'source': 'source1', 'content_hash': '哈希1'}, {'source': 'source2'， 'content_hash2':'哈希2'},...]}

            existing_hashes = set()
            
            # 提取出向量数据库中已存在文档的元数据中metadata中的哈希值content_hash
            if existing_docs_full.get("metadatas"):
            # 在正常情况下，当数据库不为空时，这个判断条件几乎总是会成功。这个检查主要是为了应对一些不可预见的异常情况，确保程序的稳定性。
                for metadata in existing_docs_full["metadatas"]:
                    if metadata and "content_hash" in metadata:
                        existing_hashes.add(metadata["content_hash"])
                        # existing_hashes集合中获取已存在于向量数据库中所有文档的metadata内的哈希值（content_hash）
            
            # 准备要添加的新文档
            new_documents = []
            new_ids = []
            
            # 将新增的文档，计算其文档document.page_content的哈希值，
            # 并添加到ids中和元数据metadata中，然后存入向量数据库
            for i, doc in enumerate(splits_documents):
                # 遍历当前每一个文档计算出哈希值
                short_hash = hash_func(doc.page_content)
                
                # 检查新加入的文档的哈希值是否已经存在于向量数据库中
                if short_hash in existing_hashes:
                    print(f"跳过重复文档 (哈希: {short_hash[:8]}...)")
                    # 如果重复，不执行append操作
                    continue
                
                # 添加哈希值到元数据
                if not doc.metadata:
                    doc.metadata = {}
                doc.metadata["content_hash"] = short_hash
                
                # 生成文档ID
                doc_id = f"doc_{short_hash}_{i}"
                new_documents.append(doc)
                new_ids.append(doc_id)
            
            # 如果新文档列表不为空，则添加新文档，
            # 如果为空，则返回空列表，不执行add_documents操作
            if new_documents:
                chroma_store.add_documents(documents=new_documents, ids=new_ids)
                print(f"添加了 {len(new_documents)} 个新文档")
            else:
                print("没有新文档需要添加")
                
        except Exception as e:
            print(f"Chroma添加文档时出错: {e}")
            
            # 使用现有的错误日志系统记录错误
            try:
                # 创建数据库管理器实例来记录错误
                db_manager = DatabaseManager()
                db_manager.log_error(
                    session_id="vector_store_system",  # 系统级错误，使用特殊session_id
                    input_text=f"向量数据库添加文档失败，尝试添加 {len(splits_documents)} 个文档", 
                    error_message=str(e), 
                    error_type="vector_store_error"
                )
                print("错误已记录到数据库")
            except Exception as log_error:
                print(f"记录错误日志时也出错: {log_error}")
            
            # 不执行降级处理，直接抛出异常，阻止文档被添加
            raise RuntimeError(f"文档添加失败: {e}")
   

class ChatModel:
    @staticmethod
    def create_chat_model(model_name="glm-4.5-flash", temperature=0.6, api_key=os.getenv("ZhiPuAI_API_KEY")):
        """创建聊天模型"""
        chat = ChatZhipuAI(
            model=model_name, 
            temperature=temperature, 
            api_key=api_key
        )
        return chat


class ConversationManager:
    """
    管理对话历史和会话的类
    负责创建和管理多轮对话的RAG链
    """
    def __init__(self, retriever):
        """
        初始化对话管理器
        
        Args:
            retriever: 向量检索器
        """
        # 存储会话历史的字典，键为session_id，值为ChatMessageHistory对象
        self.store = {}
        self.retriever = retriever
        # 创建支持历史对话的RAG链
        self.conversational_rag_chain = self._create_conversational_rag_chain()
        
    def get_session_history(self, session_id):
        """
        获取或创建会话历史记录
        
        Args:
            session_id (str): 会话ID
            
        Returns:
            ChatMessageHistory: 会话历史对象
        """
        if session_id not in self.store:
            self.store[session_id] = ChatMessageHistory()
        return self.store[session_id]
    
    def _create_conversational_rag_chain(self):
        """
        创建支持历史对话的RAG链
        
        Returns:
            RunnableWithMessageHistory: 支持历史对话的RAG链
        """
        # 问题重构器系统提示词
        # 用于根据聊天历史和当前用户问题生成一个独立的、上下文完整的查询问题
        contextualize_q_system_prompt = """
        我会给定你历史聊天信息和可能依赖历史聊天信息的最新用户问题，
        你要生成一个无需历史聊天信息也可理解的相对的独立问题。但请勿回答该问题，只需在必要时结合历史信息进行重构，
        如果没有历史信息则返回用户原始的问题。
        """

        # 问题重构器提示模板
        # 包含三部分：1. 系统指令；2. 历史对话记录；3. 用户的新问题
        contextualize_q_prompt = ChatPromptTemplate.from_messages(
            [
                ("system", contextualize_q_system_prompt), # 系统指令，指导AI怎么做事
                MessagesPlaceholder("chat_history"), # 预留位置，运行时这里会填入历史对话
                ("human", "{input}") # 预留位置，运行时这里会填入用户的新问题
            ]
        )

        # 历史感知检索器
        # 这个检索器能够根据聊天历史重构用户问题，使其更适合在向量数据库中进行检索，从而提供更准确的回答，
        # 但本身不负责回答用户的问题
        history_aware_retriever = create_history_aware_retriever(
            ChatModel.create_chat_model(), self.retriever, contextualize_q_prompt
        )

        # 问答链系统提示词
        # 这是给"研究员"的工作指令，告诉它必须严格使用检索到的资料来回答问题
        qa_system_prompt = """
        你是一名文档问答助手。请基于提供的上下文信息回答问题，不要使用外部知识或训练数据中的信息。

        **回答规则**：
        1. 如果上下文中有直接相关信息：基于上下文提供简洁准确的回答（最多三句话）
        2. 如果上下文中有部分相关信息：基于可用的上下文信息回答，并说明信息的局限性
        3. 如果上下文中完全没有相关信息：明确说明"提供的资料中没有相关信息"，并建议用户提供更具体的查询

        **禁止事项**：
        - 不要基于自身知识或训练数据回答问题
        - 不要提供一般性指导或常识性回答
        - 不要猜测或推断超出上下文范围的信息

        上下文：{context}
        """

        # 答案生成器提示模板
        # 包含：1. 系统指令；2. 历史对话；3. 用户问题
        qa_prompt = ChatPromptTemplate.from_messages(
            [
                ("system", qa_system_prompt), # 系统消息，为AI提供行为指导
                MessagesPlaceholder("chat_history"), # 占位符，运行时会被实际的聊天历史替换
                ("human", "{input}"), # 用户消息，表示用户的输入
            ]
        )

        # 创建文档问答链
        question_answer_chain = create_stuff_documents_chain(ChatModel.create_chat_model(), qa_prompt)
        # 创建检索链
        rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)

        # 创建支持历史对话的RAG链
        # 这是最终的"前台经理"，包裹着核心的流水线，并赋予了它记忆功能
        return RunnableWithMessageHistory(
            rag_chain, # 内部链，核心流水线（图书管理员+研究员）
            self.get_session_history, # 获取备忘录的函数
            input_messages_key="input", # 输入中用户问题的键名
            history_messages_key="chat_history", # 输入中历史记录的键名
            output_messages_key="answer", # 输出中答案的键名
        )


class DatabaseManager:
    """
    管理数据库连接和操作的类
    负责会话历史的持久化存储和错误日志记录
    """
    def __init__(self):
        """
        初始化数据库管理器
        """
        # 数据库配置
        self.DATABASE_URL = "mysql+pymysql://root:root@localhost:3306/chat_history?charset=utf8mb4"
        self.engine = create_engine(
            self.DATABASE_URL,
            pool_pre_ping=True,  # 每次连接前检查连接是否有效
            pool_recycle=3600,   # 连接回收时间（秒）
            echo=False           # 设置为 True 可以查看 SQL 语句（调试用）
        )

        # Base 是一个“模具声明器”。它用来定义接下来要创建的数据库表格应该长什么样子（有哪些列，什么类型）。
        # 所有的表格模具（类）都需要继承它。
        self.Base = declarative_base()

        # 作用：创建一个数据库会话的工厂函数。
        # 详细解释：
        # sessionmaker 是 SQLAlchemy 的工厂函数
        # bind=engine 将这个工厂绑定到特定的数据库引擎
        # SessionLocal 现在是一个可调用对象，每次调用都会返回一个新的数据库会话，db = SessionLocal()
        self.SessionLocal = sessionmaker(bind=self.engine)
        
        # 定义数据模型
        self._define_models()
        # 创建数据库表
        self._create_tables()
    
    def _define_models(self):
        """
        定义数据库模型
        包括会话表、消息表和错误日志表
        """
        
        class Session(self.Base):
            """
            会话表模型
            使用SQLAlchemy模型类，创建一个数据库表名为 "sessions"的表
            """
            __tablename__ = "sessions" # 指定表名，id,session_id表

            # 定义一个名为 id 的列，类型为整数(Integer)，并设置为主键(primary_key)。
            # primary_key=True 表示这是表的主键，每条记录必须有唯一的值
            # SQLAlchemy会自动让主键自增（除非明确指定不这样做）
            id = Column(Integer, primary_key=True)

            # 定义一个名为 session_id 的列，类型为字符串(String)，要求值唯一(unique)且不能为空(nullable=False)。
            # unique=True 确保每个 session_id 值在表中只出现一次，nullable=False 表示这个字段必须有值，不能为NULL
            session_id = Column(String(100), unique=True, nullable=False)

            # 定义一个关系属性，建立Session和Message类之间的关联。
            # relationship 创建了一个虚拟的关系，不是实际的数据库列
            # "Message" 指定了关联的目标模型类
            # back_populates="session" 表示在Message类中也有一个对应的关系属性叫session
            # 注意：这里的session和messages不是表名，是关系名，session和关系名messages是一对多的关系：
            # session = db.query(Session).filter(session_id=session_id).first()
            # for message in session.messages: 
            #     print(message.content)
            messages = relationship("Message", back_populates="session")
        
        class Message(self.Base):
            """
            消息表模型
            """
            __tablename__ = "messages" # 表名，id,session_id（外键是Session表中的id）,role,content表

            id = Column(Integer, primary_key=True)

            # 定义一个外键列，将消息与会话关联起来。
            # ForeignKey("sessions.id") 表示这个列引用了sessions表的id列
            # 这建立了数据库层面的引用完整性约束
            # nullable=False 必须有值，不能为NULL
            # 外键是sessions表中的id,与sessions表中的session_id完全不同
            session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)

            # 定义一个字符串列，存储消息的角色（如"human"或"ai"）
            # 这个字段标识消息是用户human发送的还是AI回复的
            # nullable=False 表示必须有值
            role = Column(String(50), nullable=False)

            # 定义一个文本列，存储消息的实际内容。
            # Text 类型可以存储大量文本，比String更适合长内容
            # nullable=False 表示消息内容不能为空
            content = Column(Text, nullable=False)

            # 关系messages和关系session是多对一的关系
            # message = db.query(Message).first()
            # owner_session = message.session  
            # print(owner_session.session_id)
            session = relationship("Session", back_populates="messages")
            
        class ErrorLog(self.Base):
            """
            错误日志表模型
            """
            __tablename__ = "error_logs" 
            
            # 类型: Integer（整数）
            # 含义: 错误记录的唯一标识符，作为主键
            # 特点: 自动递增，每条错误记录都有唯一的ID
            id = Column(Integer, primary_key=True)
            
            # 类型: String（字符串）
            # 含义: 发生错误时对应的会话ID
            # 注意: 这里只是关联标识，不是严格的外键关系
            # 用途: 追踪是哪个用户的会话出现了问题
            session_id = Column(String(100), nullable=False)
            
            # 类型: Text（文本）
            # 含义: 发生错误时用户输入的文本内容
            # 用途: 记录导致错误的用户问题或输入数据
            input_text = Column(Text)

            # 类型: Text（文本）
            # 含义: 具体的错误信息内容
            # 内容: 通常包含异常的描述、堆栈跟踪等详细信息
            # 用途: 用于调试和错误分析
            error_message = Column(Text)

            # 类型: DateTime（日期时间）
            # 含义: 错误发生的时间戳
            # 默认值: datetime.now - 自动记录插入时间
            # 用途: 便于按时间顺序排查问题
            timestamp = Column(DateTime, default=datetime.now)

            # 类型: String（字符串）
            # 含义: 错误类型分类
            # 可能的值:
            # "system" - 系统级错误
            # "database" - 数据库操作错误
            # "validation" - 输入验证错误
            # "network" - 网络连接错误等
            error_type = Column(String(50))
            
        # 保存类引用
        # 在Python中，当我们在一个方法内部定义类并将这些类赋值给self属性时，这些类引用就成为了对象的实例属性，
        # 可以在该对象的任何其他方法中通过self来访问。这不需要return语句，因为它们已经存储在对象的状态中了。
        # 实例属性: 当我们在_define_models方法中执行self.SessionModel = Session时，我们将Session类的引用存储为DatabaseManager实例的一个属性。
        # 对象状态: 一旦这些类引用被存储为实例属性，它们就成为该对象状态的一部分，保存在内存中。
        # 跨方法访问: 由于这些是实例属性（不是局部变量），所以该对象的任何其他方法都可以通过self.SessionModel、self.MessageModel和self.ErrorLogModel来访问这些类。
        # 无需return: 这与函数返回值不同，这是对象的状态管理。我们不是从方法中返回值，而是修改对象本身的状态。
        self.SessionModel = Session
        self.MessageModel = Message
        self.ErrorLogModel = ErrorLog
    
    def _create_tables(self):
        """
        创建数据库表结构
        详细解释：
        Base 是您通过 declarative_base() 创建的基类
        Base.metadata 包含了所有继承自 Base 的模型类的元数据（如表结构信息）
        create_all(engine) 会根据模型类的定义，在数据库中创建对应的表
        具体创建的表：
        根据 Session 类创建 sessions 表
        根据 Message 类创建 messages 表
        包括所有定义的列、主键、外键约束等
        为什么需要这个：
        如果数据库文件是新的或者表不存在，程序无法正常工作
        这个调用确保所需的表结构已经存在
        如果表已经存在，create_all() 不会重复创建（是幂等操作）

        注意：这段代码之前一定要先定义表的类
        """
        self.Base.metadata.create_all(self.engine)
        
    def load_all_sessions_from_db(self, store):
        """
        程序启动时从数据库加载所有会话历史到内存
        
        Args:
            store (dict): 存储会话历史的字典
        """
        db = self.SessionLocal()
        try:
            # 从数据库中查询所有会话，初次执行没有历史记录时，查询不到表则返回一个空列表，空列表不会触发for循环
            sessions = db.query(self.SessionModel).all()
            
            for session_obj in sessions:
                session_id = session_obj.session_id
                
                # 为每个会话创建空的 ChatMessageHistory 对象
                chat_history = ChatMessageHistory()
                
                # 查询该会话的所有消息，按时间顺序（这里按id排序）
                # messages 的数据结构：
                # - 类型：list（列表）
                # - 列表中的每个元素：Message 类的实例对象
                # - 示例返回值：
                #   [
                #       <Message object with id=1, session_id=1, role='human', content='第一个问题'>,
                #       <Message object with id=2, session_id=1, role='ai', content='第一个回答'>,
                #       <Message object with id=3, session_id=1, role='human', content='第二个问题'>,
                #       ...
                #   ]
                # session_id和messgaes是一对多关系，一条session_id对应多条对话消息
                messages = db.query(self.MessageModel).filter(
                    self.MessageModel.session_id == session_obj.id
                ).order_by(self.MessageModel.id).all()
                
                # 将消息添加到历史记录中
                for msg in messages:
                    if msg.role == "human":
                        chat_history.add_user_message(msg.content)
                    elif msg.role == "ai":
                        chat_history.add_ai_message(msg.content)
                
                # 将加载的历史记录存入内存store
                store[session_id] = chat_history
        except Exception as e:
            print(f"加载会话历史时出错: {e}")
        finally:
            db.close()

    def save_all_sessions(self, store):
        """
        在程序退出时将所有会话历史保存到数据库
        这是一个归档员。他的任务是：在程退出时将临时白板 (store) 上所有客户 (session_id) 的所有对话记录，
        全部整理并存入永久档案室（数据库）。
        
        Args:
            store (dict): 存储会话历史的字典
        """
        db = self.SessionLocal()
        try:
            # session_id遍历出store中的所有session_id,
            # chat_history遍历出store中的所有session_id对应的ChatMessageHistory对象列表。
            for session_id, chat_history in store.items():

                # 检查会话是否已存在
                # 类似sql查询语句：SELECT * FROM sessions WHERE session_id = 'abc123' LIMIT 1;
                # 返回的是Session的实例对象，包含id和session_id属性,如果没有找到，则返回None
                # db.query(Session).filter(Session.session_id == session_id)，返回的不是数据，而是一个 SQLAlchemy Query 对象，
                # 它代表了一个尚未执行的查询，这个 Query 对象包含了查询的所有信息（要查询什么表、有什么过滤条件等），但还没有真正去数据库执行查询，
                # 因此必须加上.first()，才能保证有值时正常返回，没值时返回None,与.one()不同。
                # .first() 的行为：
                # 返回查询结果的第一条记录，如果没有结果则返回 None
                # 即使有多条匹配记录，也只返回第一条，不会报错
                # .one() 的行为：
                # 必须恰好返回一条记录
                # 如果没有记录：抛出 sqlalchemy.orm.exc.NoResultFound 异常
                # 如果有多条记录：抛出 sqlalchemy.orm.exc.MultipleResultsFound 异常
                session_obj = db.query(self.SessionModel).filter(
                    self.SessionModel.session_id == session_id
                ).first()
                
                if not session_obj:
                    # 创建一个新的 Session 对象
                    # 使用当前遍历的 session_id 作为参数
                    # 此时对象只在内存中，还没有保存到数据库
                    session_obj = self.SessionModel(session_id=session_id)

                    # 将新创建的会话对象添加到数据库会话中
                    # 这相当于告诉 SQLAlchemy："我准备要保存这个对象到数据库了"
                    # 对象被标记为"待添加"状态，相当于刚粘贴到文档上
                    db.add(session_obj)

                    # 提交事务，将更改保存到数据库
                    # 这是真正执行数据库操作的一步，相当于粘贴到文件后点击了保存按钮
                    # 新会话记录被插入到 sessions 表中
                    # 数据库会自动为它分配一个主键 id
                    # 因为要同步新生成的id到内存中，所以不一起和message在循环外一起保存
                    db.commit()

                    # 从数据库重新加载对象，获取自动生成的主键
                    # 提交后，新记录的 id 字段被数据库自动填充
                    # refresh 确保内存中的session_obj对象也包含这个新生成的 id
                    # 这是必要的，因为后续的消息记录需要引用这个 id 作为外键
                    db.refresh(session_obj)
                
                # 保存消息
                for message in chat_history.messages:
                    # 检查消息是否已经保存过

                    existing_message = db.query(self.MessageModel).filter(
                        self.MessageModel.session_id == session_obj.id,

                        # 消息角色必须匹配（"human" 或 "ai"）
                        self.MessageModel.role == message.type,

                        self.MessageModel.content == message.content
                    ).first()
                    
                    if not existing_message:
                        new_message = self.MessageModel(
                            session_id=session_obj.id,
                            role=message.type,
                            content=message.content
                        )
                        db.add(new_message)       
            db.commit()
        except Exception as e:
            print(f"保存会话历史时出错: {e}")
            db.rollback()
        finally:
            db.close()
            
    def save_conversation_pair(self, session_id, user_input, ai_response):
        """
        专门用于保存完整对话对（用户问题 + AI回答）
        确保原子性操作
        
        Args:
            session_id (str): 会话ID
            user_input (str): 用户输入
            ai_response (str): AI回复
            
        Returns:
            bool: 保存是否成功
        """
        # 定义需要过滤的模式
        invalid_patterns = [
            # 标准化的无法回答
            "根据提供的资料，我无法回答这个问题",
            "资料中没有相关信息",
            "无法回答这个问题"
        ]
              
        # 检查AI回答是否包含需要过滤的模式，比如ai可能会回复"不知道"，会导致回复存入数据库中污染历史对话记录
        if any(pattern in ai_response for pattern in invalid_patterns):  # any() 函数用于判断给定列表是否至少有一个元素为真
            print(f"跳过保存可能使用外部知识的记录: {user_input[:50]}...")
            # 记录到错误日志
            self.log_error(session_id, user_input, "AI可能使用了外部知识或无法回答", "response_quality")
            return True  # 返回成功但不实际保存

        db = self.SessionLocal()
        try:
            # 查找或创建会话
            session = db.query(self.SessionModel).filter(
                self.SessionModel.session_id == session_id
            ).first()
            
            if not session:
                session = self.SessionModel(session_id=session_id)
                db.add(session)
                db.commit()
                db.refresh(session)
            
            # 保存用户问题
            user_message = self.MessageModel(
                session_id=session.id, 
                role="human", 
                content=user_input
            )
            db.add(user_message)
            
            # 保存AI回答
            ai_message = self.MessageModel(
                session_id=session.id, 
                role="ai", 
                content=ai_response
            )
            db.add(ai_message)
            
            db.commit()
            return True
            
        except Exception as e:
            db.rollback()
            print(f"保存对话对失败: {e}")
            self.log_error(session_id, user_input, f"保存失败: {e}", "database")
            return False
        finally:
            db.close()
            
    def log_error(self, session_id, input_text, error_message, error_type="system"):
        """
        记录错误到专门的错误日志表
        
        Args:
            session_id (str): 会话ID
            input_text (str): 用户输入文本
            error_message (str): 错误信息
            error_type (str): 错误类型
        """
        db = self.SessionLocal()
        try:
            error_log = self.ErrorLogModel(
                session_id=session_id,
                input_text=input_text,
                error_message=error_message,
                error_type=error_type
            )
            db.add(error_log)
            db.commit()
        except Exception as e:
            print(f"记录错误日志失败: {e}")
            db.rollback()
        finally:
            db.close()


class RAGApplication:
    """
    RAG应用程序主类
    整合了所有组件，提供统一的接口进行RAG对话
    """
    def __init__(self, retriever):
        """
        初始化RAG应用程序
        
        Args:
            retriever: 向量检索器
        """
        # 创建数据库管理器实例
        # 这一步在初始化时定义和创建了三个数据库
        # 在__init__创建了数据库，还没有开始调用任何其他函数
        # 
        # load_all_sessions_from_db(self, store)加载到内存方法
        # save_all_sessions(self, store)保存到数据库方法
        # save_conversation_pair(self, session_id, user_input, ai_response, store)每一对的用户和ai对话记录原子保存到数据库方法
        # log_error(self, session_id, input_text, error_message, error_type="system")保存到错误日志表方法
        self.db_manager = DatabaseManager()
        
        # 创建对话管理器实例
        # 在__init__中，创建了空字典store，并且执行了_create_conversational_rag_chain()方法，
        # # 返回：RunnableWithMessageHistory(
        #     rag_chain, # 内部链，核心流水线（图书管理员+研究员）
        #     self.get_session_history, # 获取备忘录的函数
        #     input_messages_key="input", # 输入中用户问题的键名
        #     history_messages_key="chat_history", # 输入中历史记录的键名
        #     output_messages_key="answer", # 输出中答案的键名
        # )对象
        # 
        # 初始化时传入了retriver调度器，定义了空的store字典，
        # 可以调用了创建支持历史对话的RAG链方法：_create_conversational_rag_chain(self)，返回ai的回复
        self.conversation_manager = ConversationManager(retriever)
        
        # 加载数据库中的聊天历史记录写入store中
        self.db_manager.load_all_sessions_from_db(self.conversation_manager.store)

        # 注册程序退出时的保存函数
        # 详细解释：
        # atexit 是 Python 的内置模块，用于注册在程序正常退出时执行的函数
        # register() 方法告诉 Python："当程序要退出时，请调用我指定的这个函数"
        # save_all_sessions 是您定义的函数，负责将所有内存中的会话历史保存到数据库
        # 为什么需要这个：
        # 您的 store 字典是内存中的临时存储
        # 如果程序意外退出（如用户按 Ctrl+C、程序崩溃等），内存中的数据会丢失
        # 这个注册确保无论程序如何退出，都会尝试保存数据
        # 就像设置一个自动保存机制，确保即使突然断电，也能在最后一刻保存重要文件。
        # 
        # 与version5不同的是，这里的store不是全局变量，是ConversationManager类的实例属性，因此需要手动传入
        atexit.register(self.db_manager.save_all_sessions, self.conversation_manager.store)
    
    def invoke_and_save(self, session_id, input_text):
        """
        使用专用的对话对保存函数
        
        Args:
            session_id (str): 会话ID
            input_text (str): 用户输入文本
            
        Returns:
            str: AI回复内容
        """
        # 输入验证
        if not input_text or not input_text.strip():
            error_msg = "输入内容不能为空，请重新输入问题。"
            # 记录到错误日志
            self.db_manager.log_error(session_id, "(空输入)", error_msg, "validation")
            print(error_msg)
            return error_msg
        
        try:
            # 先调用RAG链获取ai回答
            # 获取self.conversation_manager作为
            # ConversationManager(retriever)的实例对象，后面.conversational_rag_chain
            # 用来获取ConversationManager(retriever)的属性conversational_rag_chain
            result = self.conversation_manager.conversational_rag_chain.invoke(
                {"input": input_text},
                config={"configurable": {"session_id": session_id}}  
            )["answer"]       
            
            print(f"用户问题: {input_text}\nAI回答: {result}")
 
            # 使用专用函数保存对话对
            if not self.db_manager.save_conversation_pair(
                session_id, input_text, result
            ):
                error_msg = "对话记录保存失败，但AI已回答您的问题。"
                print(error_msg)
                return f"{result}\n\n注意：{error_msg}"
            else:
                return result
                
        except Exception as e:
            error_msg = "抱歉，系统暂时无法处理您的请求，请稍后再试。"
            print(f"系统错误: {str(e)}")
            # 记录系统错误
            self.db_manager.log_error(session_id, input_text, str(e), "system")
            return error_msg
# ... existing code ...


if __name__ == "__main__": 
    # agent英文文档
    # page_url = "https://lilianweng.github.io/posts/2023-06-23-agent/"
    # class_tuple = ("post-content", "post-title", "post-header")

    # agent中文文档
    # page_url = "https://blog.csdn.net/universsky2015/article/details/144695013"
    # class_tuple = ("htmledit_views atelier-sulphurpool-light", "article-title-box", "article-header")
    
    # 花语百科
    # page_url = "https://zhidao.baidu.com/question/1552083454601228467.html"
    # class_tuple = ("rich-content-container rich-text-",)

    # web前端作用
    # page_url = "https://blog.csdn.net/m0_52335699/article/details/128097643?ops_request_misc=%257B%2522request%255Fid%2522%253A%252252c2518846dccc743059c63fd249a25e%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=52c2518846dccc743059c63fd249a25e&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-128097643-null-null.142^v102^pc_search_result_base6&utm_term=web%E5%89%8D%E7%AB%AF%E4%BD%9C%E7%94%A8&spm=1018.2226.3001.4187"
    # class_tuple = ("blog-content-box",)

    # 稀土元素
    page_url = "https://baike.baidu.com/item/%E7%A8%80%E5%9C%9F%E5%85%83%E7%B4%A0/5631744"
    class_tuple = ("J-lemma-content",)
    
    splits_documents = DocumentProcessor(page_url, class_tuple).process()

     # 使用带持久化和哈希去重的版本
    persist_dir = "./chroma_persist"
    retriever = ChromaStore.get_retriever("embedding-3", splits_documents, persist_directory=persist_dir)

    # 创建RAG应用程序实例
    app = RAGApplication(retriever)
    
    # 进行多轮对话测试
    session_id = "abc91xxx"
    
    # 第一轮对话
    result1 = app.invoke_and_save(session_id, "帮我概述这篇文章")
    print(f"第一轮对话结果: {result1}\n")
    
    # 第二轮对话（基于历史）
    result2 = app.invoke_and_save(session_id, "这篇文章有什么用")
    print(f"第二轮对话结果: {result2}\n")
    
    # 第三轮对话（基于历史）
    result3 = app.invoke_and_save(session_id, "我国稀土产业现状")
    print(f"第三轮对话结果: {result3}\n")
    
    # 使用完毕后清除向量数据库
    # 方法1：删除整个集合（推荐）
    # ChromaStore.delete_collection("embedding-3")
    
    # 方法2：只清除文档内容，保留集合结构
    # ChromaStore.clear_all_documents("embedding-3")
    
    
    # # "*********************************************************************************************************************************************"
    # # "实现历史对话功能"
    # # 想象一个智能的图书馆系统，它由一位图书管理员（Librarian） 和一位研究员（Researcher） 组成。你和他们进行多轮对话。
    # # 你（用户）：前来问问题的人。
    # # 历史聊天记录（Chat History）：你和研究员之前对话的备忘录。
    # # 图书管理员（history_aware_retriever）：他的工作是听懂你的新问题，并结合之前的对话备忘录，重新组织一个更清晰、更完整的问题，然后去巨大的书库（向量数据库）里帮你找到最相关的书籍（文档）。他本身不回答问题，只负责找书。
    # # 书库（retriever + 向量数据库）：存储所有书籍（文档）的地方。
    # # 研究员（question_answer_chain）：他的工作是阅读图书管理员找来的书籍，然后用自己的话简洁地回答你的问题。他非常依赖管理员找来的资料。
    # # 会话管理器（RunnableWithMessageHistory）：他是整个图书馆的前台经理。他认得每一位访客（通过session_id），并为每位访客单独保管着一份对话备忘录（ChatMessageHistory）。每次你来了，他都会把对应的备忘录交给图书管理员和研究员，确保他们记得之前和你聊过什么，从而使对话能连贯地进行下去。
    
    # # 解释：这是给“图书管理员”的工作指令。它告诉管理员：“当你听到用户的新问题时，
    # # 先看看我们之前的对话备忘录。如果新问题和之前聊的有关（比如用了‘那’、‘它’这类代词），
    # # 你就把问题补充完整，变成一个 standalone （独立）的问题。如果完全是新问题，就不用修改。
    # # 记住，你的任务是重新组织问题，而不是回答问题。”
    # contextualize_q_system_prompt = """
    #     我会给定你历史聊天信息和可能依赖历史聊天信息的最新用户问题，
    #     你要生成一个无需历史聊天信息也可理解的相对的独立问题。但请勿回答该问题，只需在必要时结合历史信息进行重构，
    #     如果没有历史信息则返回用户原始的问题。
    # """

    # # 问题重构器
    # # 用于根据聊天历史和当前用户问题生成一个独立的、上下文完整的查询问题。
    # # 解释：这是一个“模板”，规定了每次询问“图书管理员”时应该给他看哪些信息。
    # # 模板包含三部分：1. 他的工作指令；2. 历史对话记录；3. 用户的新问题。
    # contextualize_q_prompt = ChatPromptTemplate.from_messages(
    #     [
    #         ("system", contextualize_q_system_prompt), # 系统指令，指导AI怎么做事
    #         MessagesPlaceholder("chat_history"), # 预留位置，运行时这里会填入历史对话
    #         ("human", "{input}") # 预留位置，运行时这里会填入用户的新问题
    #     ]
    # )

    # # 历史感知检索器
    # # 这个检索器能够根据聊天历史重构用户问题，使其更适合在向量数据库中进行检索，从而提供更准确的回答。
    # # 解释：这就是创建我们比喻中的“图书管理员”。
    # # ChatModel.create_chat_model()：是管理员的大脑（AI模型），让他能理解指令和重构问题。
    # # retriever：是管理员身后的书库入口，他知道如何进去找书。
    # # contextualize_q_prompt：是图书管理员的工作手册（模板）。
    # # 这个管理员的特点是：他会先根据历史和当前问题重构出一个更好的问题，再用这个新问题去书库里检索。
    # history_aware_retriever = create_history_aware_retriever(ChatModel.create_chat_model(), retriever, contextualize_q_prompt)

    # # 问答链
    # # 解释：这是给“研究员”的工作指令。它告诉研究员：“你是回答问题的人。
    # # 你必须严格使用图书管理员为你找来的资料（{context}） 来回答问题。
    # # 回答要简短（最多三句）。如果资料里没有答案，就老实说不知道。”
    # qa_system_prompt = """
    #     你是一名文档问答助手。请基于提供的上下文信息回答问题，不要使用外部知识或训练数据中的信息。

    #     **回答规则**：
    #     1. 如果上下文中有直接相关信息：基于上下文提供简洁准确的回答（最多三句话）
    #     2. 如果上下文中有部分相关信息：基于可用的上下文信息回答，并说明信息的局限性
    #     3. 如果上下文中完全没有相关信息：明确说明"提供的资料中没有相关信息"，并建议用户提供更具体的查询

    #     **禁止事项**：
    #     - 不要基于自身知识或训练数据回答问题
    #     - 不要提供一般性指导或常识性回答
    #     - 不要猜测或推断超出上下文范围的信息

    #     上下文：{context}
    #     """
    
    # # 答案生成器
    # # 解释：这是“研究员”的工作模板。
    # # 包含：1. 他的工作指令；2. 历史对话（让他回答得更连贯）；3. 用户的问题。
    # qa_prompt = ChatPromptTemplate.from_messages(
    #     [
    #         ("system", qa_system_prompt), # 系统消息，为AI提供行为指导
    #         MessagesPlaceholder("chat_history"), # 占位符，运行时会被实际的聊天历史替换
    #         ("human", "{input}"), # 用户消息，表示用户的输入
    #     ]
    # ) 

    # # 解释：这就是创建“研究员”。他的工作模式是：
    # # “Stuffing”——即把图书管理员找来的所有文档（资料）“塞”进模板里的{context}位置，
    # # 然后结合指令、历史和问题，生成一个答案。
    # question_answer_chain = create_stuff_documents_chain(ChatModel.create_chat_model(), qa_prompt)
    
    # # 解释：这里把“图书管理员”和“研究员”组装成一条流水线。这条流水线的工作流程是：
    # # 用户输入问题。
    # # 图书管理员接手：结合历史，重构问题，去书库检索相关文档。
    # # 研究员接手：拿到管理员检索到的文档，阅读后生成答案。
    # # 输出最终答案。
    # rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)

    # # 工作流程图：
    # # 用户提问："那它们的化学性质怎么样？"
    # #         ↓
    # # contextualize_q_prompt (结合历史对话)
    # #         ↓ 重构问题
    # # "稀土元素的化学性质怎么样？"
    # #         ↓ 向量检索
    # # [相关文档片段]
    # #         ↓
    # # qa_prompt (结合历史对话 + 文档)
    # #         ↓ 生成回答
    # # "稀土元素具有相似的化学性质..."

    # # 这就是“前台经理”用来管理备忘录的方法。他用一个字典（store）来记录所有访客。
    # # 每个访客有一个唯一的session_id（如abc123）。当访客第一次来时（session_id不在store中），
    # # 就为他创建一个新的空备忘录（ChatMessageHistory()。下次同一个人再来时，
    # # 经理就能拿出他之前的备忘录。
    #     # ChatMessageHistory 类似夹具（Fixture） 或一个对话状态容器。它的工作流程是在一个对话回合（Turn）中扮演着承前启后的关键角色：
    #     # 回合开始（读取）：当一个新用户问题进来时，RunnableWithMessageHistory 会调用 get_session_history(session_id) 这个“夹具”来获取之前的完整对话历史。这个历史被用作上下文，帮助理解和处理当前的新问题。
    #     # 回合进行（使用）：检索器和生成链利用这个历史上下文来处理问题并生成答案。
    #     # 回合结束（写入）：在AI回答完用户的问题之后，RunnableWithMessageHistory 会自动地将本次回合的用户问题（HumanMessage）和AI回复（AIMessage）作为一个原子操作，追加存入到同一个 ChatMessageHistory “夹具”中。
    #     # 这个过程就像一个完美的循环，确保了每次对话都能“记住”之前的所有内容
    # # 存储的数据类似：
    #     # store = {
    #     #     "abc123": ChatMessageHistory(
    #     #         messages=[
    #     #             HumanMessage(content="agents 是什么？"),
    #     #             AIMessage(content="Agents是能够自主决策..."),  # AI的回答内容
    #     #             HumanMessage(content="那有什么作用呢？"),
    #     #             AIMessage(content="Agents的作用是...")  # AI的回答内容
    #     #         ]
    #     #     ),
    #     #     "def456": ChatMessageHistory(
    #     #         messages=[
    #     #             HumanMessage(content="什么是机器学习？"),
    #     #             AIMessage(content="机器学习是...")  # AI的回答内容
    #     #         ]
    #     #     )
    #     # }
    # store = {}
    # def get_session_history(session_id):
    #     if session_id not in store:
    #         store[session_id] = ChatMessageHistory()
    #     return store[session_id]
    
    # # 解释：这就是创建最终的“前台经理”（RunnableWithMessageHistory）。他包裹着核心的流水线，并赋予了它记忆功能。
    # # 他的工作是：每当有用户提问时，他就根据用户的session_id调用get_session_history函数找到这个用户的专属备忘录：store。
    # # 然后，他把用户当前问题和找到的历史备忘录一起交给核心流水线（rag_chain）去处理。
    # # 最后，流水线产生的新的问答对又会被自动追加到这位用户的备忘录里，为下一次对话做好准备。这就是实现多轮对话的关键。
    # conversational_rag_chain = RunnableWithMessageHistory(
    #     rag_chain, # 内部链，核心流水线（图书管理员+研究员）
    #     get_session_history, # 获取备忘录的函数
    #     input_messages_key="input", # 输入中用户问题的键名

    #     history_messages_key="chat_history", # 输入中历史记录的键名
    
    #     output_messages_key="answer", # 输出中答案的键名
    # )

    # def load_all_sessions_from_db():
    #     """程序启动时从数据库加载所有会话历史到内存"""
    #     db = SessionLocal()
    #     try:
    #         # 查询所有会话，初次执行没有历史记录时，查询不到表则返回一个空列表，空列表不会触发for循环
    #         sessions = db.query(Session).all()
            
    #         for session_obj in sessions:
    #             session_id = session_obj.session_id
                
    #             # 为每个会话创建 ChatMessageHistory 对象
    #             chat_history = ChatMessageHistory()
                
    #             # 查询该会话的所有消息，按时间顺序（这里按id排序）
    #             # messages 的数据结构：
    #             # - 类型：list（列表）
    #             # - 列表中的每个元素：Message 类的实例对象
    #             # - 示例返回值：
    #             #   [
    #             #       <Message object with id=1, session_id=1, role='human', content='第一个问题'>,
    #             #       <Message object with id=2, session_id=1, role='ai', content='第一个回答'>,
    #             #       <Message object with id=3, session_id=1, role='human', content='第二个问题'>,
    #             #       ...
    #             #   ]
    #             # session_id和messgaes是一对多关系，一条session_id对应多条对话消息
    #             messages = db.query(Message).filter(
    #                 Message.session_id == session_obj.id
    #             ).order_by(Message.id).all()
                
    #             # 将消息添加到历史记录中
    #             for msg in messages:
    #                 if msg.role == "human":
    #                     chat_history.add_user_message(msg.content)
    #                 elif msg.role == "ai":
    #                     chat_history.add_ai_message(msg.content)
                
    #             # 将加载的历史记录存入内存store
    #             store[session_id] = chat_history
            
    #     except Exception as e:
    #         print(f"加载会话历史时出错: {e}")
    #     finally:
    #         db.close()

    # # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # # Base 是一个“模具声明器”。它用来定义接下来要创建的数据库表格应该长什么样子（有哪些列，什么类型）。
    # # 所有的表格模具（类）都需要继承它。
    # Base = declarative_base()

    # # 使用 mysql数据库（就像一个独立的文件柜）。
    # # 数据库文件保存在chat_history数据库中，与sqlite不同的是，mysql要指定端口号，而且还要现创建数据库名称：chat_history
    # DATABASE_URL = "mysql+pymysql://root:root@localhost:3306/chat_history?charset=utf8mb4"

    # # 这是创建了一个档案室管理员 (engine)。
    # # 他的唯一工作就是知道如何连接和操作那个 chat_history文件柜。后续所有对数据库的操作都要通过他。
    # # 创建引擎时可以添加一些参数以适应 MySQL
    # engine = create_engine(
    #     DATABASE_URL,
    #     pool_pre_ping=True,  # 每次连接前检查连接是否有效
    #     pool_recycle=3600,   # 连接回收时间（秒）
    #     echo=False           # 设置为 True 可以查看 SQL 语句（调试用）
    # )

    # class Session(Base):
    #     """
    #         使用SQLAlchemy模型类，创建一个数据库表名为 "sessions"的表
    #     """
    #     __tablename__ = "sessions" # 指定表名，id,session_id表

    #     # 定义一个名为 id 的列，类型为整数(Integer)，并设置为主键(primary_key)。
    #     # primary_key=True 表示这是表的主键，每条记录必须有唯一的值
    #     # SQLAlchemy会自动让主键自增（除非明确指定不这样做）
    #     id = Column(Integer, primary_key=True)

    #     # 定义一个名为 session_id 的列，类型为字符串(String)，要求值唯一(unique)且不能为空(nullable=False)。
    #     # unique=True 确保每个 session_id 值在表中只出现一次，nullable=False 表示这个字段必须有值，不能为NULL
    #     session_id = Column(String(100), unique=True, nullable=False)

    #     # 定义一个关系属性，建立Session和Message类之间的关联。
    #     # relationship 创建了一个虚拟的关系，不是实际的数据库列
    #     # "Message" 指定了关联的目标模型类
    #     # back_populates="session" 表示在Message类中也有一个对应的关系属性叫session
    #     # 注意：这里的session和messages不是表名，是关系名，session和关系名messages是一对多的关系：
    #     # session = db.query(Session).filter(session_id=session_id).first()
    #     # for message in session.messages: 
    #     #     print(message.content)
    #     messages = relationship("Message", back_populates="session")

    # class Message(Base):
    #     __tablename__ = "messages" # 表名，id,session_id（外键是Session表中的id）,role,content表

    #     id = Column(Integer, primary_key=True)

    #     # 定义一个外键列，将消息与会话关联起来。
    #     # ForeignKey("sessions.id") 表示这个列引用了sessions表的id列
    #     # 这建立了数据库层面的引用完整性约束
    #     # nullable=False 必须有值，不能为NULL
    #     # 外键是sessions表中的id,与sessions表中的session_id完全不同
    #     session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)

    #     # 定义一个字符串列，存储消息的角色（如"human"或"ai"）
    #     # 这个字段标识消息是用户human发送的还是AI回复的
    #     # nullable=False 表示必须有值
    #     role = Column(String(50), nullable=False)

    #     # 定义一个文本列，存储消息的实际内容。
    #     # Text 类型可以存储大量文本，比String更适合长内容
    #     # nullable=False 表示消息内容不能为空
    #     content = Column(Text, nullable=False)

    #     # 关系messages和关系session是多对一的关系
    #     # message = db.query(Message).first()
    #     # owner_session = message.session  
    #     # print(owner_session.session_id)
    #     session = relationship("Session", back_populates="messages")

    # # 错误日志表
    # class ErrorLog(Base):
    #     __tablename__ = "error_logs" 
        
    #     # 类型: Integer（整数）
    #     # 含义: 错误记录的唯一标识符，作为主键
    #     # 特点: 自动递增，每条错误记录都有唯一的ID
    #     id = Column(Integer, primary_key=True)
        
    #     # 类型: String（字符串）
    #     # 含义: 发生错误时对应的会话ID
    #     # 注意: 这里只是关联标识，不是严格的外键关系
    #     # 用途: 追踪是哪个用户的会话出现了问题
    #     session_id = Column(String(100), nullable=False)
        
    #     # 类型: Text（文本）
    #     # 含义: 发生错误时用户输入的文本内容
    #     # 用途: 记录导致错误的用户问题或输入数据
    #     input_text = Column(Text)

    #     # 类型: Text（文本）
    #     # 含义: 具体的错误信息内容
    #     # 内容: 通常包含异常的描述、堆栈跟踪等详细信息
    #     # 用途: 用于调试和错误分析
    #     error_message = Column(Text)

    #     # 类型: DateTime（日期时间）
    #     # 含义: 错误发生的时间戳
    #     # 默认值: datetime.now - 自动记录插入时间
    #     # 用途: 便于按时间顺序排查问题
    #     timestamp = Column(DateTime, default=datetime.now)

    #     # 类型: String（字符串）
    #     # 含义: 错误类型分类
    #     # 可能的值:
    #     # "system" - 系统级错误
    #     # "database" - 数据库操作错误
    #     # "validation" - 输入验证错误
    #     # "network" - 网络连接错误等
    #     error_type = Column(String(50))

    # # 作用：创建数据库表结构
    # # 详细解释：
    # # Base 是您通过 declarative_base() 创建的基类
    # # Base.metadata 包含了所有继承自 Base 的模型类的元数据（如表结构信息）
    # # create_all(engine) 会根据模型类的定义，在数据库中创建对应的表
    # # 具体创建的表：
    # # 根据 Session 类创建 sessions 表
    # # 根据 Message 类创建 messages 表
    # # 包括所有定义的列、主键、外键约束等
    # # 为什么需要这个：
    # # 如果数据库文件是新的或者表不存在，程序无法正常工作
    # # 这个调用确保所需的表结构已经存在
    # # 如果表已经存在，create_all() 不会重复创建（是幂等操作）
    # # 注意：这段代码之前一定要先定义表的类
    # Base.metadata.create_all(engine)

    # # 作用：创建一个数据库会话的工厂函数。
    # # 详细解释：
    # # sessionmaker 是 SQLAlchemy 的工厂函数
    # # bind=engine 将这个工厂绑定到特定的数据库引擎
    # # SessionLocal 现在是一个可调用对象，每次调用都会返回一个新的数据库会话，db = SessionLocal()
    # SessionLocal = sessionmaker(bind=engine)
 
    # def log_error(session_id, input_text, error_message, error_type="system"):
    #     """记录错误到专门的错误日志表"""
    #     db = SessionLocal()
    #     try:
    #         error_log = ErrorLog(
    #             session_id=session_id,
    #             input_text=input_text,
    #             error_message=error_message,
    #             error_type=error_type
    #         )
    #         db.add(error_log)
    #         db.commit()
    #     except Exception as e:
    #         print(f"记录错误日志失败: {e}")
    #         db.rollback()
    #     finally:
    #         db.close()

    # # save_message 函数：实时保存
    # # 调用时机：用户和AI每完成一轮对话（用户提问 + AI回答）后立即调用
    # # 保存内容：当前这一轮对话的两条记录
    # # 用户的提问（role: "human"）
    # # AI的回答（role: "ai"）
    # # 特点：实时性高，确保每条对话都能及时持久化
    # # 使用场景：正常的对话交互过程中

    # # save_all_sessions 函数：批量备份
    # # 调用时机：程序异常退出时（通过 atexit.register 注册）
    # # 保存内容：内存中所有会话的所有聊天记录
    # # 特点：安全网机制，防止意外情况导致数据丢失
    # # 使用场景：
    # # 程序崩溃
    # # 突然断电
    # # 非正常关闭聊天窗口
    # # 其他意外退出情况

    # # 这种设计的优势
    # # 这种"实时保存 + 批量备份"的双重保障设计有几个重要优势：
    # # 1. 数据安全性最大化
    # # 正常情况下，每条对话都实时保存，数据丢失风险极低
    # # 异常情况下，有最后的安全网兜底，尽可能挽回数据
    # # 2. 性能平衡
    # # 实时保存虽然有一定开销，但确保了数据的及时性
    # # 批量备份只在异常退出时触发，不影响正常使用性能
    # # 3. 容错能力强
    # # 即使实时保存偶尔失败（如网络问题），还有批量备份作为补偿
    # # 两个机制互为补充，大大提高了系统的可靠性

    # def save_all_sessions():
    #     """
    #     这是一个归档员。他的任务是：在程退出时将临时白板 (store) 上所有客户 (session_id) 的所有对话记录，
    #     全部整理并存入永久档案室（数据库）。
    #     """

    #     # SessionLocal() 就像是向档案室管理员申请一次临时的、集中的“归档工作权限” (db)。
    #     # 在这段时间内，你可以进行多次添加、修改档案的操作，最后统一提交db.commit()。
    #     db = SessionLocal()
        
    #     try:
                   
    #         # session_id遍历出store中的所有session_id,
    #         # chat_history遍历出store中的所有session_id对应的ChatMessageHistory对象列表。
    #         for session_id, chat_history in store.items():

    #             # 检查会话是否已存在
    #             # 类似sql查询语句：SELECT * FROM sessions WHERE session_id = 'abc123' LIMIT 1;
    #             # 返回的是Session的实例对象，包含id和session_id属性,如果没有找到，则返回None
    #             # db.query(Session).filter(Session.session_id == session_id)，返回的不是数据，而是一个 SQLAlchemy Query 对象，
    #             # 它代表了一个尚未执行的查询，这个 Query 对象包含了查询的所有信息（要查询什么表、有什么过滤条件等），但还没有真正去数据库执行查询，
    #             # 因此必须加上.first()，才能保证有值时正常返回，没值时返回None,与.one()不同。
    #             # .first() 的行为：
    #             # 返回查询结果的第一条记录，如果没有结果则返回 None
    #             # 即使有多条匹配记录，也只返回第一条，不会报错
    #             # .one() 的行为：
    #             # 必须恰好返回一条记录
    #             # 如果没有记录：抛出 sqlalchemy.orm.exc.NoResultFound 异常
    #             # 如果有多条记录：抛出 sqlalchemy.orm.exc.MultipleResultsFound 异常
    #             session_obj = db.query(Session).filter(Session.session_id == session_id).first()
                
    #             if not session_obj:

    #                 # 创建一个新的 Session 对象
    #                 # 使用当前遍历的 session_id 作为参数
    #                 # 此时对象只在内存中，还没有保存到数据库
    #                 session_obj = Session(session_id=session_id)

    #                 # 将新创建的会话对象添加到数据库会话中
    #                 # 这相当于告诉 SQLAlchemy："我准备要保存这个对象到数据库了"
    #                 # 对象被标记为"待添加"状态，相当于刚粘贴到文档上
    #                 db.add(session_obj)

    #                 # 提交事务，将更改保存到数据库
    #                 # 这是真正执行数据库操作的一步，相当于粘贴到文件后点击了保存按钮
    #                 # 新会话记录被插入到 sessions 表中
    #                 # 数据库会自动为它分配一个主键 id
    #                 # 因为要刷新出新的id，所以不一起和message在循环外一起保存
    #                 db.commit()

    #                 # 从数据库重新加载对象，获取自动生成的主键
    #                 # 提交后，新记录的 id 字段被数据库自动填充
    #                 # refresh 确保内存中的对象也包含这个新生成的 id
    #                 # 这是必要的，因为后续的消息记录需要引用这个 id 作为外键
    #                 db.refresh(session_obj)
                
    #             # 保存消息
    #             for message in chat_history.messages:
    #                 # 检查消息是否已经保存过

    #                 existing_message = db.query(Message).filter(
    #                     Message.session_id == session_obj.id,

    #                     # 消息角色必须匹配（"human" 或 "ai"）
    #                     Message.role == message.type,

    #                     Message.content == message.content
    #                 ).first()
                    
    #                 if not existing_message:
    #                     new_message = Message(
    #                         session_id=session_obj.id,
    #                         role=message.type,
    #                         content=message.content
    #                     )
    #                     db.add(new_message)
            
    #         db.commit()
    #     except Exception as e:
    #         print(f"保存会话历史时出错: {e}")
    #         db.rollback()
    #     finally:
    #         db.close()   
    
    # def get_db():
    #     """
    #     创建一个实用程序函数来管理数据库会话。
    #     该函数将确保每个数据库会话正确打开和关闭
    #     """
    #     db = SessionLocal()
    #     try:
    #         yield db
    #     finally:
    #         db.close()

    # def save_conversation_pair(session_id, user_input, ai_response):
    #     """
    #     专门用于保存完整对话对（用户问题 + AI回答）
    #     确保原子性操作
    #     """
    #     # 定义需要过滤的模式
    #     invalid_patterns = [
    #         # 标准化的无法回答
    #         "根据提供的资料，我无法回答这个问题",
    #         "资料中没有相关信息",
    #         "无法回答这个问题"
    #     ]
              
    #     # 检查AI回答是否包含需要过滤的模式，比如ai可能会回复“不知道”，会导致回复存入数据库中污染历史对话记录
    #     if any(pattern in ai_response for pattern in invalid_patterns):  # any() 函数用于判断给定列表是否至少有一个元素为真
    #         print(f"跳过保存可能使用外部知识的记录: {user_input[:50]}...")
    #         # 记录到错误日志
    #         log_error(session_id, user_input, "AI可能使用了外部知识或无法回答", "response_quality")
    #         return True  # 返回成功但不实际保存

    #     db = next(get_db())
    #     try:
    #         # 查找或创建会话
    #         session = db.query(Session).filter(Session.session_id == session_id).first()
    #         if not session:
    #             session = Session(session_id=session_id)
    #             db.add(session)
    #             db.commit()
    #             db.refresh(session)
            
    #         # 保存用户问题
    #         user_message = Message(session_id=session.id, role="human", content=user_input)
    #         db.add(user_message)
            
    #         # 保存AI回答
    #         ai_message = Message(session_id=session.id, role="ai", content=ai_response)
    #         db.add(ai_message)
            
    #         db.commit()
    #         return True
            
    #     except Exception as e:
    #         db.rollback()
    #         print(f"保存对话对失败: {e}")
    #         log_error(session_id, user_input, f"保存失败: {e}", "database")
    #         return False
    #     finally:
    #         db.close()

    # def invoke_and_save(session_id, input_text):
    #     """使用专用的对话对保存函数"""
        
    #     if not input_text or not input_text.strip():
    #         error_msg = "输入内容不能为空，请重新输入问题。"
            
    #         log_error(session_id, "(空输入)", error_msg, "validation")
    #         print(error_msg)
    #         return error_msg
        
    #     try:
    #         # 先调用RAG链获取ai回答
    #         result = conversational_rag_chain.invoke(
    #             {"input": input_text},
    #             config={"configurable": {"session_id": session_id}}  
    #         )["answer"]        
            
    #         print(f"用户问题: {input_text}\nAI回答: {result}")

    #         # 使用专用函数保存对话对
    #         if not save_conversation_pair(session_id, input_text, result):
    #             error_msg = "对话记录保存失败，但AI已回答您的问题。"
    #             print(error_msg)
    #             return f"{result}\n\n注意：{error_msg}"
    #         else:
    #             return result
            
    #     except Exception as e:
    #         error_msg = "抱歉，系统暂时无法处理您的请求，请稍后再试。"
    #         print(f"系统错误: {str(e)}")

    #         log_error(session_id, input_text, str(e), "system")
    #         return error_msg
        
    # # 详细解释：
    # # atexit 是 Python 的内置模块，用于注册在程序正常退出时执行的函数
    # # register() 方法告诉 Python："当程序要退出时，请调用我指定的这个函数"
    # # save_all_sessions 是您定义的函数，负责将所有内存中的会话历史保存到数据库
    # # 为什么需要这个：
    # # 您的 store 字典是内存中的临时存储
    # # 如果程序意外退出（如用户按 Ctrl+C、程序崩溃等），内存中的数据会丢失
    # # 这个注册确保无论程序如何退出，都会尝试保存数据
    # # 就像设置一个自动保存机制，确保即使突然断电，也能在最后一刻保存重要文件。
    # atexit.register(save_all_sessions)
    
    # # 将数据库中的聊天历史记录写入store中
    # load_all_sessions_from_db()

    # # invoke_and_save("abc8", "钪是什么？")
    # # invoke_and_save("abc8", "那它有啥作用啊？")
    # invoke_and_save("abc8", "那有什么特性呢？")
    # # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    # # 解释：访客abc123第一次提问：“agents 是什么？”。
    # # 经理发现这是新访客，创建空备忘录。
    # # 经理把问题"agents 是什么？"和空备忘录[]交给流水线。
    # # 图书管理员看到历史是空的，所以直接用原问题去书库检索。
    # # 研究员根据检索到的文档，生成答案（first_ans）。
    # # 经理自动将用户问题和系统答案记入abc123的备忘录。现在备忘录是：[Human: "agents 是什么？", AI: "(第一个答案)"]
    # # first_ans = conversational_rag_chain.invoke(
    # #     {"input": "web前端是什么？"}, # 第一个问题
    # #     config={
    # #         "configurable": {"session_id": "abc123"} # 告诉经理：“我是访客abc123”
    # #     },
    # # )["answer"]
    
    # # 解释：同一个访客abc123继续提问：“那有什么作用呢？”。
    # # 经理认出他是abc123，拿出他的备忘录（里面包含第一轮问答）。
    # # 经理把新问题"那有什么作用呢？"和之前的备忘录一起交给流水线。
    # # 关键步骤：图书管理员看到新问题里有个“那”，他查看备忘录，发现上一句在聊“agents”，于是他重构了问题，将“那”具体化。新问题可能变成了：“agents 有什么作用呢？”。
    # # 他用这个重构后的、完整的问题去书库检索，找到更相关的文档。
    # # 研究员根据这些新文档，生成一个连贯的答案（second_ans）。
    # # 经理再把这一轮新的问答追加到abc123的备忘录里。
    # # second_ans = conversational_rag_chain.invoke(
    # #     {"input": "有什么作用？"},
    # #     config={
    # #         "configurable": {"session_id": "abc123"}
    # #     },
    # # )["answer"]

    # # print("first_ans:{}".format(first_ans))
    # # print("second_ans:{}".format(second_ans))

    # # 运行流程：
    # # 第1步：调用开始
    # # second_ans = conversational_rag_chain.invoke(
    # #     {"input": "那有什么作用呢？"}, // 用户输入
    # #     config={"configurable": {"session_id": "abc123"}} // 会话ID
    # # )

    # # 第2步：RunnableWithMessageHistory 接管
    # # 它读取 config 中的 session_id: "abc123"。
    # # 它调用您定义的 get_session_history("abc123") 函数。
    # # 这个函数从 store 字典里返回属于 abc123 的 ChatMessageHistory 对象。
    # # 假设这个对象里已经存储了第一次的对话：
    # # [
    # #     HumanMessage(content="agents 是什么？"),
    # #     AIMessage(content="Agents是...（第一个答案）")
    # # ]

    # #  第3步：组装输入
    # # RunnableWithMessageHistory 现在开始准备真正要传给内部 rag_chain 的输入数据。它会把：
    # # 用户的新输入 {"input": "那有什么作用呢？"}
    # # 和 获取到的历史记录 "chat_history": [HumanMessage(...), AIMessage(...)]
    # # 合并成一个新的字典（dict）作为输入：
    # # {
    # #     "input": "那有什么作用呢？",
    # #     "chat_history": [
    # #         HumanMessage(content="agents 是什么？"),
    # #         AIMessage(content="Agents是...（第一个答案）")
    # #     ]
    # # }

    # # 第4步：执行内部rag_chain
    # # 这个组装好的字典被送入 rag_chain 执行。rag_chain 内部又包含两步：先检索，再生成答案。
    
    # # 第5步：检索器重构问题（使用历史）
    # # history_aware_retriever 会收到同样的输入字典（包含 input 和 chat_history）。
    # # 它利用自身的模板 contextualize_q_prompt，将历史 (chat_history) 和当前问题 (input) 格式化，让LLM生成一个独立问题，比如“agents 有什么作用呢？”。
    # # 然后用这个新问题去向量数据库检索，得到相关的文档片段 documents。

    # # 第6步：生成链生成答案（再次使用历史）
    # # 现在轮到 question_answer_chain 工作了。它也会收到一个组装好的输入，其中包含：
    # # "input": "那有什么作用呢？"
    # # "chat_history": [...] （同样的历史消息）
    # # "context": documents （上一步检索到的文档列表）
    # # 关键一步：question_answer_chain 使用它的模板 qa_prompt 来格式化最终给LLM的提示。
    # # ("system", qa_system_prompt) -> 系统指令
    # # MessagesPlaceholder("chat_history") -> 被替换为 [HumanMessage(...), AIMessage(...)] 这个列表
    # # ("human", "{input}") -> 被替换为 "那有什么作用呢？"
    # # 最终，一个巨大的、包含指令、完整对话历史和当前问题的提示被发送给LLM，LLM据此生成连贯的回答。
   
    # # 第7步：保存历史
    # # 调用结束后，RunnableWithMessageHistory 会自动将本次的 HumanMessage（用户问题）和 AIMessage（AI回答）追加到 session_id 为 abc123 的 ChatMessageHistory 对象中，为下一次对话做好准备。

    # # "*********************************************************************************************************************************************"


    # # rag_res = RAGChain(retriever).invoke("学习agent需要学习哪些内容？")
    # # print(rag_res)

    #  # 使用完毕后清除向量数据库
    # # 方法1：删除整个集合（推荐）
    # ChromaStore.delete_collection("embedding-3")
    
    # 方法2：只清除文档内容，保留集合结构
    # ChromaStore.clear_all_documents("embedding-3")


